寫測試程式時,經常會遇到這樣的問題:「我寫的測試夠嗎?
」、「還有哪些程式碼沒有被測試到?
」
Code Coverage(程式碼涵蓋範圍)
是用來回答這些問題的指標。它會告訴你測試執行時,實際覆蓋了多少比例的程式碼。
但需要先說明一個重點:Code Coverage 不是萬能的。100% 的涵蓋率不代表沒有 Bug,也不表示測試品質良好。它只是一個輔助指標,幫助你找出可能遺漏的測試區域。
今天會介紹如何在不同開發環境中使用 Code Coverage 工具,包括 VS Code 和 Visual Studio 的相關工具設定,以及如何正確解讀涵蓋率報告。
程式碼涵蓋範圍是一種測量指標,用來統計測試執行時實際執行了多少程式碼。
Bug
,你的測試應該要使用或「覆蓋」大部分的程式碼。錯誤認知:
正確認知:
當 Code Coverage 被當作 KPI 時,開發者會為了衝數字而寫沒有 Assert 的測試,完全失去了測試的意義。
延伸閱讀:
Visual Studio 有內建的程式碼涵蓋範圍功能:
限制:
也可以用 .NET CLI 工具:
# 安裝工具
dotnet tool install -g dotnet-coverage
# 執行測試並產生報告
dotnet-coverage collect dotnet test
相關文檔:
限制:
沒有 Visual Studio Enterprise 的話,安裝 Visual Studio Extensions - Fine Code Coverage
是最好的選擇:
相關連結:
開啟選項:工具 → 選項 → Fine Code Coverage
啟用功能:
設定完成後,執行所有測試:
測試執行完,Fine Code Coverage 會顯示報告:
啟用編輯器指示器:工具 > FCC Toggle Indicators
程式碼編輯器中的顏色標示:
圖片來源: Fine Code Coverage - Visual Studio Marketplace
根據報告進行改善:
在某些 Windows 作業系統環境下,.NET 8/9 的安全性更新可能會讓 Fine Code Coverage 無法正常運作,可參考以下文章內容進行相關設定:
詳細解決方法參考:
- 使用 Fine Code Coverage 取得程式碼覆蓋範圍 | mrkt的程式學習筆記
- 查看
Fine Code Coverage 無法產生 Code Coverage 報告的解決方式
內容
如果你使用 VS Code 開發,也可以在裡面進行測試覆蓋率分析。VS Code 的測試功能支援多種語言和框架,對跨平台開發很有用。
安裝測試擴充套件
Ctrl+Shift+X
開啟擴充功能C# Dev Kit
)開啟測試總管
Testing: Focus on 測試總管 View
相關連結
VS Code 提供幾種方式檢視覆蓋率:
在 測試總管
中可以:
執行涵蓋範圍測試
」以涵蓋範圍執行
」顯示樹狀結構的覆蓋率資訊:
直接在程式碼中標示:
Test: Show Inline Coverage (測試: 切換內嵌涵蓋範圍)
(Ctrl+; Ctrl+Shift+I)在檔案總管中直接顯示各檔案的覆蓋率百分比,方便快速識別需要加強測試的檔案。
可以調整的設定項目:
設定 | 用途 |
---|---|
testing.countBadge |
活動列測試圖示的計數顯示 |
testing.gutterEnabled |
編輯器邊欄測試控制項 |
testing.defaultGutterClickAction |
邊欄控制項預設動作 |
testing.coverageBarThresholds |
覆蓋率顏色閾值 |
testing.displayedCoveragePercent |
覆蓋率百分比顯示類型 |
testing.showCoverageInExplorer |
檔案總管覆蓋率顯示 |
項目 | VS Code | Visual Studio |
---|---|---|
平台支援 | Windows, macOS, Linux | 主要 Windows |
語言支援 | 多語言(透過擴充) | 主要 .NET 語言 |
測試框架 | 廣泛支援 | MSTest, xUnit, NUnit |
覆蓋率工具 | 內建支援 | Enterprise 或第三方 |
適用場景 | 跨平台、多語言 | .NET 企業級專案 |
以下工具為 Visual Studio 的 Extensions (不是 VS Code 的 Extensions)
除了 Code Coverage,還可以用程式碼可維護性指標評估測試需求:
CodeMaintainability 提供的指標:
Maintainability Index (0-100):可維護性指數
Cyclomatic Complexity:循環複雜度
Halstead Volume:程式碼體積
Lines of Code:程式碼行數
循環複雜度(Cyclomatic Complexity)與測試案例的關係
循環複雜度不僅是衡量程式碼邏輯複雜度的指標,也是一個非常實用的參考依據,可以用來判斷「至少」要寫多少個單元測試案例。
為什麼循環複雜度可以當作測試案例的下限?
循環複雜度的數值代表了程式中所有獨立邏輯路徑(independent paths)的數量。為了達到 完整的邏輯覆蓋(branch coverage)
,理論上你需要撰寫至少等同於循環複雜度數值的測試案例,以涵蓋所有可能的邏輯分支。
public int Max(int[] array)
{
if (array == null || array.Length == 0)
{
throw new ArgumentException("array must not be empty.");
}
int max = array[0];
for (int i = 1; i < array.Length; i++)
{
if (array[i] > max)
{
max = array[i];
}
}
return max;
}
在實務中,我們可以用一個簡化的方式來估算:
每個條件判斷(如 if、for、while、case、&&、||)都會增加 1
Max 方法的循環複雜度分析:
Max 這個方法的 循環複雜度為 5
,代表它有 5 條獨立的邏輯路徑。這也表示:
至少需要 5 個單元測試案例
才能涵蓋所有邏輯分支 (每個邏輯路徑都被執行過一次)。測試案例設計:
以下是根據 Max(int[] array) 方法的循環複雜度為 5 所設計的 5 個單元測試案例
,每個案例都對應一條獨立的邏輯路徑,確保涵蓋所有條件與分支:
測試案例 | 測試內容 | 涵蓋路徑 |
---|---|---|
Max_傳入null_應拋出例外 |
傳入 null | array == null |
Max_傳入空陣列_應拋出例外 |
傳入空陣列 | array.Length == 0 |
Max_陣列只有單一元素_應回傳該元素 |
單一元素 | 不進入迴圈 |
Max_最大值在開頭_應回傳最大值 |
最大值在開頭 | 迴圈不更新 max |
Max_最大值在中間_應回傳最大值 |
最大值在中間 | 迴圈更新 max |
CodeMaid 是另一個實用的 Visual Studio 擴充套件:
開啟 CodeMaid Spade 檢視程式碼結構:
Spade 功能:
實際專案中可能出現高複雜度警告:
當看到紅色數字時:
影響:
基於需求分析:
參考複雜度指標:
平衡覆蓋率與品質:
今天的重點:
單元測試案例的數量可以根據程式碼的複雜度和重要性來決定。可以使用以下方法來確定測試案例的數量:
從前面的介紹與文章內容可以得知 CodeMaid 是對於開發人員在編寫程式碼時的一個輔助工具,但似乎沒有提到對於寫單元測試有什麼直接的幫助。
前面一直都有提到很多開發人員對於單元測試最在乎的一件事「要寫多少的測試案例?
」
可以藉由 CodeMaid 所提供的一個小功能來得到一個粗略的數字,而這個數字不是一個相當準確的答案,但至少可以讓開發人員藉此來取得「至少要寫多少個測試案例
」的參考依據。
我的建議會是開發者依據需求 (SA 或 SD 規格),去寫出功能的 使用案例
,就是這個功能的 使用說明書
,以使用這個方法的 User 角度去描述這個功能要怎麼使用,例如:
而這些使用案例逐一列出來之後,我們就會知道程式會怎麼開發、要寫哪些的測試案例來驗證實作程式碼。而這些使用案例也就可以轉換作為測試案例。
實際的工作專案可能就無法依據 Code Maintainability 或 Code Metric 就可以判斷要寫多少測試案例,複雜度與可維護性的數據就只能當作參考。
專案開發時就會真的需要將使用案例逐一列出,接著依據需求規格與使用案例將程式實作出來,在實作的過程中就會知道有哪些情境會需要測試。
要寫多少個測試案例並沒有一個標準,甚至連達到 100% 程式碼涵蓋率的程式碼也並不表示就寫完了所有的測試案例。因為資料的多樣性與變化、需求規格的複雜等因素,對程式開發人員來說也只能盡量地讓已知的測試情境去寫出來。
系統能夠正確地運作執行並不代表該系統的品質就是好的
,只是都沒有執行到會出錯的情境而已,一旦出現了當初設計、開發時沒有設想到的情境時,系統就會出現錯誤,而這就是系統的 BUG,然後為了要修復這個 BUG 而要去修改程式碼。
對一個沒有單元測試覆蓋的程式碼去做異動,就如同矇著眼睛走進一個到處埋著地雷的危險地帶。沒有單元測試的保護,異動程式碼任何一個地方都無法保證不會對既有功能有影響,甚至於影響到什麼地方也都充滿著未知。
所以
目前我們已經掌握測試基礎、xUnit 框架、Awesome Assertions 和 Code Coverage。但寫單元測試時,經常遇到一個問題:如何處理外部相依性?
當程式碼需要存取資料庫、呼叫 API、讀取檔案或使用系統時間時,這些相依性會讓測試:
明天會介紹 「依賴替代入門:使用 NSubstitute」:
這是從基礎測試進階到實用單元測試的關鍵步驟。
範例程式碼:
這是「重啟挑戰:老派軟體工程師的測試修練」的第六天。明天會介紹 Day 07:依賴替代入門 - 使用 NSubstitute。